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 */; }; FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5582BAB767000FD8220 /* Tips.swift */; };
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */; }; FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */; };
FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DF49A2BD8D23900822FA0 /* BarButtonView.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 */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; 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 */; }; FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; };
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.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 */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; };
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
@ -642,6 +674,7 @@
C425D4042B6D249E002A7B48 /* Assets.xcassets */, C425D4042B6D249E002A7B48 /* Assets.xcassets */,
FFF024192BF48AEE001F14B4 /* Localization */, FFF024192BF48AEE001F14B4 /* Localization */,
FF0EC54D2BB195CA0056B6D1 /* CSV */, FF0EC54D2BB195CA0056B6D1 /* CSV */,
FF1F4B802BFA0105000B4573 /* HTML Templates */,
C425D4062B6D249E002A7B48 /* Preview Content */, C425D4062B6D249E002A7B48 /* Preview Content */,
); );
path = PadelClub; path = PadelClub;
@ -896,10 +929,28 @@
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */, FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */,
FF5D0D882BB4935C005CB568 /* ClubRowView.swift */, FF5D0D882BB4935C005CB568 /* ClubRowView.swift */,
FFC91B022BD85E2400B29808 /* CourtView.swift */, FFC91B022BD85E2400B29808 /* CourtView.swift */,
FF53FBB62BFB301A0051D4C3 /* Shared */,
); );
path = Club; path = Club;
sourceTree = "<group>"; 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 */ = { FF39719B2B8DE04B004C4E75 /* Navigation */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -918,6 +969,8 @@
children = ( children = (
FF70916B2B91005400AB08DA /* TournamentView.swift */, FF70916B2B91005400AB08DA /* TournamentView.swift */,
FF8F26402BADFC8700650388 /* TournamentInitView.swift */, FF8F26402BADFC8700650388 /* TournamentInitView.swift */,
FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */,
FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */,
FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */, FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */,
FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */, FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */,
FF3F74F92B91A018004CFE0E /* Screen */, FF3F74F92B91A018004CFE0E /* Screen */,
@ -947,6 +1000,7 @@
FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, FF1162802BCF945C000C4809 /* TournamentCashierView.swift */,
FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */, FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */,
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */, FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */,
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */,
FF8F26522BAE0E4E00650388 /* Components */, FF8F26522BAE0E4E00650388 /* Components */,
); );
path = Screen; path = Screen;
@ -1004,6 +1058,14 @@
path = ViewModel; path = ViewModel;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FF53FBB62BFB301A0051D4C3 /* Shared */ = {
isa = PBXGroup;
children = (
FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */,
);
path = Shared;
sourceTree = "<group>";
};
FF5D30542BD95AF600F2B93D /* Ongoing */ = { FF5D30542BD95AF600F2B93D /* Ongoing */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1232,6 +1294,8 @@
C49EF0432BE286780077B5AA /* Key.swift */, C49EF0432BE286780077B5AA /* Key.swift */,
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */, FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */,
FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */, FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */,
FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */,
FF1F4B732BFA00FC000B4573 /* HtmlService.swift */,
FF8F26352BAD523300650388 /* PadelRule.swift */, FF8F26352BAD523300650388 /* PadelRule.swift */,
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, FFF8ACD32B92392C008466FA /* SourceFileManager.swift */,
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
@ -1396,6 +1460,16 @@
FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */, FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */,
FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */, FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */,
FF0EC5502BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-12-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 */, FF0EC5512BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv in Resources */,
FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */, FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */,
FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */, FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */,
@ -1470,6 +1544,7 @@
FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */,
FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */, FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */,
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */,
FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */, FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */,
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */, FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */,
@ -1530,6 +1605,7 @@
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */,
FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */,
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
@ -1595,11 +1671,13 @@
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */,
FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */,
FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */,
FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */, FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */,
FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */,
@ -1628,6 +1706,7 @@
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStageSettingsView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStageSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
@ -1659,6 +1738,7 @@
FFC91B012BD85C2F00B29808 /* Court.swift in Sources */, FFC91B012BD85C2F00B29808 /* Court.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */,
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */,
FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */,
FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */, FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */,
@ -1847,7 +1927,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 9; CURRENT_PROJECT_VERSION = 11;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1885,7 +1965,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 9; CURRENT_PROJECT_VERSION = 11;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -35,9 +35,9 @@ class Club : ModelObject, Storable, Hashable {
var zipCode: String? var zipCode: String?
var latitude: Double? var latitude: Double?
var longitude: 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.creator = creator
self.name = name self.name = name
self.acronym = acronym ?? name.acronym() self.acronym = acronym ?? name.acronym()
@ -48,6 +48,7 @@ class Club : ModelObject, Storable, Hashable {
self.zipCode = zipCode self.zipCode = zipCode
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.courtCount = courtCount
} }
func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String { func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String {
@ -63,12 +64,12 @@ class Club : ModelObject, Storable, Hashable {
return URLs.main.url.appending(path: "?club=\(id)") return URLs.main.url.appending(path: "?club=\(id)")
} }
var courts: [Court] { var customizedCourts: [Court] {
Store.main.filter { $0.club == self.id }.sorted(by: \.index) Store.main.filter { $0.club == self.id }.sorted(by: \.index)
} }
override func deleteDependencies() throws { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.courts) try Store.main.deleteDependencies(items: self.customizedCourts)
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -83,6 +84,7 @@ class Club : ModelObject, Storable, Hashable {
case _zipCode = "zipCode" case _zipCode = "zipCode"
case _latitude = "latitude" case _latitude = "latitude"
case _longitude = "longitude" case _longitude = "longitude"
case _courtCount = "courtCount"
} }
} }

@ -149,6 +149,18 @@ class GroupStage: ModelObject, Storable {
return _matches().filter { matchIndexes.contains($0.index) } 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] { func availableToStart() -> [Match] {
let runningMatches = runningMatches() let runningMatches = runningMatches()
return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })

@ -124,6 +124,30 @@ class Match: ModelObject, Storable {
return startDate?.addingTimeInterval(minutesToAdd * 60.0) 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() { func resetMatch() {
losingTeamId = nil losingTeamId = nil
winningTeamId = nil winningTeamId = nil
@ -542,6 +566,11 @@ class Match: ModelObject, Storable {
return winningTeamId == team?.id 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? { func team(_ team: TeamPosition) -> TeamRegistration? {
if groupStage != nil { if groupStage != nil {
switch team { switch team {

@ -25,7 +25,7 @@ extension Club {
} }
static func newEmptyInstance() -> 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 { enum State {
case initial case initial
case build case build
case running
case canceled case canceled
case finished
} }
func publishedTeamsDate() -> Date { func publishedTeamsDate() -> Date {
@ -400,8 +402,15 @@ class Tournament : ModelObject, Storable {
if self.isCanceled == true { if self.isCanceled == true {
return .canceled 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 .build
} }
return .initial return .initial
@ -1411,7 +1420,7 @@ class Tournament : ModelObject, Storable {
} }
func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { 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 { 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 addition
case edition case edition
case lockedForEditing case lockedForEditing
case selection
} }
enum DisplayStyle { 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) { 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 { func fetchData() async {
if let mostRecent = mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent { await fetchData(fromDate: Date())
await fetchData(fromDate: current) // if let mostRecent = mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent {
} else { // await fetchData(fromDate: current)
await fetchData(fromDate: Date()) // } else {
} // }
} }
func _removeAllData(fromDate current: Date) { func _removeAllData(fromDate current: Date) {
@ -96,11 +96,11 @@ class SourceFileManager {
try await group.waitForAll() try await group.waitForAll()
} }
if current < Date() { // if current < Date() {
if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) { // if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) {
await fetchData(fromDate: nextCurrent) // await fetchData(fromDate: nextCurrent)
} // }
} // }
} catch { } catch {
print("downloadRankingData", error) print("downloadRankingData", error)

@ -304,20 +304,12 @@ struct MultiTournamentsEventTip: Tip {
} }
var message: Text? { 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? { var image: Image? {
Image(systemName: "trophy.circle") 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 { 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 { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -12,6 +12,7 @@ enum URLs: String, Identifiable {
case main = "https://xlr.alwaysdata.net/" case main = "https://xlr.alwaysdata.net/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app" //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 } var id: String { return self.rawValue }

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

@ -192,7 +192,8 @@ struct CallMessageCustomizationView: View {
if let eventClub = tournament.eventObject()?.clubObject() { if let eventClub = tournament.eventObject()?.clubObject() {
let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: dataStore.user.id) let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: dataStore.user.id)
Section { Section {
TextField("Nom du club", text: $customClubName) TextField("Nom du club", text: $customClubName, axis: .vertical)
.lineLimit(2)
.autocorrectionDisabled() .autocorrectionDisabled()
.focused($focusedField, equals: .clubName) .focused($focusedField, equals: .clubName)
.onSubmit { .onSubmit {
@ -204,8 +205,6 @@ struct CallMessageCustomizationView: View {
} }
} }
.disabled(hasBeenCreated == false) .disabled(hasBeenCreated == false)
} header: {
Text("Nom du club")
} footer: { } footer: {
if hasBeenCreated == false { if hasBeenCreated == false {
Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) 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 { var body: some View {
Form { Form {
Section { Section {
TextField("Nom de l'événement", text: $eventName) TextField("Nom de l'événement", text: $eventName, axis: .vertical)
.lineLimit(2)
.autocorrectionDisabled() .autocorrectionDisabled()
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity)
.onSubmit { .onSubmit {
if eventName.trimmed.isEmpty == false { if eventName.trimmed.isEmpty == false {
event.name = eventName.trimmed event.name = eventName.trimmed
@ -32,6 +35,8 @@ struct EventSettingsView: View {
} }
_save() _save()
} }
} header: {
Text("Nom de l'événement")
} footer: { } footer: {
if eventName.isEmpty == false { if eventName.isEmpty == false {
FooterButtonView("effacer le nom") { FooterButtonView("effacer le nom") {

@ -9,17 +9,21 @@ import SwiftUI
import LeStorage import LeStorage
struct ClubDetailView: View { struct ClubDetailView: View {
@Bindable var club: Club
var displayContext: DisplayContext
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@FocusState var focusedField: Club.CodingKeys? @FocusState var focusedField: Club.CodingKeys?
@State private var acronymMode: Club.AcronymMode = .automatic @State private var acronymMode: Club.AcronymMode = .automatic
@State private var city: String @State private var city: String
@State private var zipCode: 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) _club = Bindable(club)
self.displayContext = displayContext self.displayContext = displayContext
self.selection = selection
_acronymMode = State(wrappedValue: club.shortNameMode()) _acronymMode = State(wrappedValue: club.shortNameMode())
_city = State(wrappedValue: club.city ?? "") _city = State(wrappedValue: club.city ?? "")
_zipCode = State(wrappedValue: club.zipCode ?? "") _zipCode = State(wrappedValue: club.zipCode ?? "")
@ -27,43 +31,26 @@ struct ClubDetailView: View {
var body: some View { var body: some View {
Form { Form {
Section { Section {
NavigationLink { TextField("Nom du club", text: $club.name, axis: .vertical)
ClubSearchView(displayContext: .edition, club: club) .lineLimit(2)
} label: { .autocorrectionDisabled()
Label("Chercher dans la base fédérale", systemImage: "magnifyingglass") .keyboardType(.alphabet)
} .frame(maxWidth: .infinity)
} footer: { .focused($focusedField, equals: ._name)
Text("Vous pouvez chercher un club dans la base fédérale et importer les informations directement.") .submitLabel( displayContext == .addition ? .next : .done)
} .onSubmit {
if club.acronym.isEmpty {
Section { club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
LabeledContent { focusedField = ._city
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
}
} }
} label: { if displayContext == .addition {
Text("Nom du club") focusedField = ._acronym
} }
.onTapGesture { }
focusedField = ._name
}
LabeledContent { LabeledContent {
if acronymMode == .automatic { if acronymMode == .automatic || displayContext == .lockedForEditing {
Text(club.acronym) Text(club.acronym)
} else { } else {
TextField("Nom court", text: $club.acronym) TextField("Nom court", text: $club.acronym)
@ -98,6 +85,7 @@ struct ClubDetailView: View {
} label: { } label: {
Text(acronymMode.rawValue) Text(acronymMode.rawValue)
} }
.disabled(displayContext == .lockedForEditing)
} }
} }
.onChange(of: acronymMode) { .onChange(of: acronymMode) {
@ -106,8 +94,16 @@ struct ClubDetailView: View {
club.acronym = "" 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 { LabeledContent {
TextField("Ville", text: $city) TextField("Ville", text: $city)
.autocorrectionDisabled() .autocorrectionDisabled()
@ -146,17 +142,15 @@ struct ClubDetailView: View {
.onTapGesture { .onTapGesture {
focusedField = ._zipCode focusedField = ._zipCode
} }
} footer: {
if displayContext == .lockedForEditing {
Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed)
}
} }
.disabled(displayContext == .lockedForEditing)
} 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)
ClubCourtSetupView(club: club, displayContext: displayContext, selectedCourt: $selectedCourt)
if let federalLink = club.federalLink() { if let federalLink = club.federalLink() {
Section { Section {
@ -167,11 +161,51 @@ struct ClubDetailView: View {
Text(club.city ?? "") Text(club.city ?? "")
} }
Link(destination: federalLink) { 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 { if displayContext == .edition {
Section { Section {
RowButtonView("Supprimer ce club", role: .destructive) { RowButtonView("Supprimer ce club", role: .destructive) {
@ -192,22 +226,10 @@ struct ClubDetailView: View {
.navigationTitle(displayContext == .addition ? "Nouveau club" : "Détail du club") .navigationTitle(displayContext == .addition ? "Nouveau club" : "Détail du club")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar(.visible, for: .navigationBar) .toolbar(.visible, for: .navigationBar)
.headerProminence(.increased)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .navigationDestination(item: $selectedCourt) { court in
if displayContext == .edition || displayContext == .lockedForEditing { CourtView(court: court)
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)
}
}
} }
.onDisappear { .onDisappear {
if displayContext == .edition { if displayContext == .edition {

@ -9,10 +9,11 @@ import SwiftUI
struct ClubImportView: View { struct ClubImportView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var selection: ((Club) -> ())? = nil
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ClubSearchView(displayContext: .addition) ClubSearchView(displayContext: .addition, selection: selection)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {

@ -9,11 +9,14 @@ import SwiftUI
struct ClubRowView: View { struct ClubRowView: View {
let club: Club let club: Club
var displayContext: DisplayContext = .edition
var body: some View { var body: some View {
LabeledContent { LabeledContent {
Image(systemName: club.isFavorite() ? "star.fill" : "star") // if displayContext == .edition {
.foregroundStyle(club.isFavorite() ? .green : .logoRed) // Image(systemName: club.isFavorite() ? "star.fill" : "star")
// .foregroundStyle(club.isFavorite() ? .green : .logoRed)
// }
} label: { } label: {
Text(club.name) Text(club.name)
Text(club.acronym) Text(club.acronym)

@ -26,10 +26,20 @@ struct ClubSearchView: View {
@State private var getForwardCityList: [CLPlacemark] = [] @State private var getForwardCityList: [CLPlacemark] = []
@State private var searchPresented: Bool = false @State private var searchPresented: Bool = false
@State private var showingSettingsAlert = 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 displayContext: DisplayContext = .edition
var club: Club? var club: Club?
var selection: ((Club) -> ())? = nil
fileprivate class DebouncableViewModel: ObservableObject { fileprivate class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = "" @Published var debouncableText: String = ""
@ -162,7 +172,7 @@ struct ClubSearchView: View {
if searchAttempted { if searchAttempted {
RowButtonView("Créer un club manuellement") { 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")) ContentUnavailableView("recherche en cours", systemImage: "mappin.and.ellipse", description: Text("recherche des clubs autour de vous"))
} }
} }
.sheet(isPresented: $presentClubCreationView) { .sheet(isPresented: presentClubCreationView) {
CreateClubView() if let newClub {
CreateClubView(club: newClub) { club in
selection?(club)
dismiss()
}
.tint(.master) .tint(.master)
}
} }
.alert(isPresented: $showingSettingsAlert) { .alert(isPresented: $showingSettingsAlert) {
Alert( Alert(
@ -295,8 +310,8 @@ struct ClubSearchView: View {
private func _importClub(clubToEdit: Club, clubMarker: ClubMarker) { private func _importClub(clubToEdit: Club, clubMarker: ClubMarker) {
if clubToEdit.creator == dataStore.user.id { if clubToEdit.creator == dataStore.user.id {
if clubToEdit.name.isEmpty { if clubToEdit.name.isEmpty {
clubToEdit.name = clubMarker.nom clubToEdit.name = clubMarker.nom.capitalized
clubToEdit.acronym = clubToEdit.automaticShortName() clubToEdit.acronym = clubToEdit.automaticShortName().capitalized
} }
clubToEdit.code = clubMarker.clubID clubToEdit.code = clubMarker.clubID
clubToEdit.latitude = clubMarker.lat clubToEdit.latitude = clubMarker.lat
@ -316,6 +331,7 @@ struct ClubSearchView: View {
} }
} }
dismiss() dismiss()
selection?(clubToEdit)
} }
private func _filteredClubs() -> [ClubMarker] { private func _filteredClubs() -> [ClubMarker] {

@ -12,20 +12,21 @@ import LeStorage
struct ClubsView: View { struct ClubsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var presentClubCreationView: Bool = false
@State private var presentClubSearchView: Bool = false @State private var presentClubSearchView: Bool = false
let tip = SlideToDeleteTip() @State private var newClub: Club?
var selection: ((Club) -> ())? = nil var selection: ((Club) -> ())? = nil
var presentClubCreationView: Binding<Bool> { Binding(
get: { newClub != nil },
set: { isPresented in
if isPresented == false {
newClub = nil
}
}
)}
var body: some View { var body: some View {
List { List {
//
// if dataStore.clubs.isEmpty == false && selection == nil {
// Section {
// TipView(tip)
// .tipStyle(tint: nil)
// }
// }
let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: true) let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: true)
ForEach(clubs) { club in ForEach(clubs) { club in
if let selection { if let selection {
@ -33,7 +34,7 @@ struct ClubsView: View {
selection(club) selection(club)
dismiss() dismiss()
} label: { } label: {
ClubRowView(club: club) ClubRowView(club: club, displayContext: .selection)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -62,7 +63,7 @@ struct ClubsView: View {
Text("Texte décrivant l'utilité d'un club et les features que cela apporte") Text("Texte décrivant l'utilité d'un club et les features que cela apporte")
} actions: { } actions: {
RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") { RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") {
presentClubCreationView = true newClub = Club.newEmptyInstance()
} }
RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") { RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") {
presentClubSearchView = true presentClubSearchView = true
@ -71,13 +72,27 @@ struct ClubsView: View {
} }
} }
.navigationTitle(selection == nil ? "Mes clubs" : "Choisir un club") .navigationTitle(selection == nil ? "Mes clubs" : "Choisir un club")
.sheet(isPresented: $presentClubCreationView) { .navigationBarTitleDisplayMode(.inline)
CreateClubView() .toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: presentClubCreationView) {
if let newClub {
CreateClubView(club: newClub) { club in
if let selection {
selection(club)
dismiss()
}
}
.tint(.master) .tint(.master)
}
} }
.sheet(isPresented: $presentClubSearchView) { .sheet(isPresented: $presentClubSearchView) {
ClubImportView() ClubImportView() { club in
.tint(.master) if let selection {
selection(club)
dismiss()
}
}
.tint(.master)
} }
.toolbar { .toolbar {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
@ -91,7 +106,7 @@ struct ClubsView: View {
} }
Button { Button {
presentClubCreationView = true newClub = Club.newEmptyInstance()
} label: { } label: {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.resizable() .resizable()

@ -9,18 +9,14 @@ import SwiftUI
import LeStorage import LeStorage
struct CreateClubView: View { struct CreateClubView: View {
@Bindable var club: Club
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var club: Club
init() { var selection: ((Club) -> ())? = nil
self.club = Club.newEmptyInstance()
}
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ClubDetailView(club: club, displayContext: .addition) ClubDetailView(club: club, displayContext: .addition, selection: selection)
.task { .task {
do { do {
try await dataStore.clubs.loadDataFromServerIfAllowed() try await dataStore.clubs.loadDataFromServerIfAllowed()
@ -55,6 +51,7 @@ struct CreateClubView: View {
self.dataStore.saveUser() self.dataStore.saveUser()
} }
dismiss() dismiss()
selection?(existingOrCreatedClub)
} }
.disabled(club.isValid == false) .disabled(club.isValid == false)
} }
@ -64,6 +61,6 @@ struct CreateClubView: View {
} }
#Preview { #Preview {
CreateClubView() CreateClubView(club: Club.mock())
.environmentObject(DataStore.shared) .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: { Button(action: {
action() action()
}) { }) {
Label { Image(systemName: icon)
Text(accessibilityLabel) .resizable()
} icon: { .scaledToFit()
Image(systemName: icon) .frame(minHeight: 28)
.resizable()
.scaledToFit() /*
.frame(minHeight: 36) Label {
} Text(accessibilityLabel)
.labelStyle(.iconOnly) } 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: { } label: {
if let selectedClub { if let selectedClub {
ClubRowView(club: selectedClub) ClubRowView(club: selectedClub, displayContext: .selection)
} else { } else {
Text("Choisir un club") 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() .autocorrectionDisabled()
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity)
LabeledContent { LabeledContent {
Text(tournaments.count.formatted()) Text(tournaments.count.formatted())
} label: { } 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 { switch eventType {
case .approvedTournament: case .approvedTournament:
approvedTournamentEditorView approvedTournamentEditorView
@ -110,7 +106,7 @@ struct EventCreationView: View {
} }
tournaments.forEach { tournament in tournaments.forEach { tournament in
tournament.courtCount = selectedClub?.courts.count ?? 2 tournament.courtCount = selectedClub?.courtCount ?? 2
tournament.startDate = startingDate tournament.startDate = startingDate
tournament.dayDuration = duration tournament.dayDuration = duration
tournament.setupFederalSettings() tournament.setupFederalSettings()
@ -134,10 +130,23 @@ struct EventCreationView: View {
dismiss() 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") .navigationTitle("Nouvel événement")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.onAppear {
if selectedClub == nil {
selectedClub = dataStore.user.clubsObjects().first
}
}
} }
} }

@ -66,7 +66,6 @@ struct MainView: View {
.environmentObject(dataStore) .environmentObject(dataStore)
.task { .task {
await self._checkSourceFileAvailability() await self._checkSourceFileAvailability()
await self._downloadPreviousDate()
} }
// .refreshable { // .refreshable {
// Task { // Task {
@ -99,8 +98,8 @@ struct MainView: View {
} }
} }
} }
} else if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable { } else if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, let lastDataSourceDate = SourceFileManager.shared.lastDataSourceDate() {
if mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast { if mostRecentDateAvailable > lastDataSourceDate {
Label(mostRecentDateAvailable.monthYearFormatted + " disponible", systemImage: "exclamationmark.triangle") Label(mostRecentDateAvailable.monthYearFormatted + " disponible", systemImage: "exclamationmark.triangle")
.labelStyle(.titleAndIcon) .labelStyle(.titleAndIcon)
} else { } else {

@ -10,9 +10,13 @@ import SwiftUI
struct OngoingView: View { struct OngoingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore @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] { 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 { var body: some View {
@ -54,10 +58,10 @@ struct OngoingView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Menu { Menu {
Button("Par terrain") { Picker(selection: $sortByField) {
Text("Trier par date").tag(false)
} Text("Trier par terrain").tag(true)
Button("Par date") { } label: {
} }
//todo //todo
@ -72,7 +76,7 @@ struct OngoingView: View {
} }
ToolbarItem(placement: .status) { ToolbarItem(placement: .status) {
if matches.isEmpty == false { 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: { } footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.") 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) .navigationTitle(TabDestination.toolbox.title)
} }

@ -34,24 +34,18 @@ struct PlanningSettingsView: View {
SubscriptionInfoView() SubscriptionInfoView()
Section { Section {
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) DatePicker(selection: $tournament.startDate) {
Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized)
}
LabeledContent { LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1) StepperView(count: $tournament.dayDuration, minimum: 1)
} label: { } label: {
Text("Durée") Text("Durée")
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
} }
} header: {
Text("Démarrage et durée du tournoi")
}
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) 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() { if let event = tournament.eventObject() {
NavigationLink { NavigationLink {
CourtAvailabilitySettingsView(event: event) CourtAvailabilitySettingsView(event: event)
@ -61,13 +55,43 @@ struct PlanningSettingsView: View {
} }
} }
} footer: { } footer: {
FooterButtonView((showOptions ? "masquer" : "voir") + " les réglages avancées") { if let club = tournament.club() {
showOptions.toggle() 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 { NavigationLink {
_optionsView() List {
_optionsView()
}
.navigationTitle("Options avancées")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} label: {
Text("Voir les options avancées")
} }
Section { Section {
@ -132,6 +156,13 @@ struct PlanningSettingsView: View {
@ViewBuilder @ViewBuilder
private func _optionsView() -> some View { 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 { Section {
Toggle(isOn: $matchScheduler.randomizeCourts) { Toggle(isOn: $matchScheduler.randomizeCourts) {

@ -11,7 +11,8 @@ import LeStorage
struct TournamentClubSettingsView: View { struct TournamentClubSettingsView: View {
@Environment(Tournament.self) private var tournament: Tournament @Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State var selectedCourt: Court? @State private var selectedCourt: Court?
@State private var showClubDetail: Club?
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
@ -19,101 +20,54 @@ struct TournamentClubSettingsView: View {
let event = tournament.eventObject() let event = tournament.eventObject()
let selectedClub = event?.clubObject() let selectedClub = event?.clubObject()
Section { Section {
if let selectedClub { NavigationLink {
NavigationLink { ClubsView() { club in
ClubDetailView(club: selectedClub, displayContext: selectedClub.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing) if let event {
} label: { event.club = club.id
ClubRowView(club: selectedClub) do {
} try dataStore.events.addOrUpdate(instance: event)
} else { } catch {
NavigationLink { Logger.error(error)
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") Text("Choisir un club")
} }
} }
} header: { } header: {
Text("Lieu du tournoi") Text("Lieu du tournoi")
} footer: { } footer: {
if let event, selectedClub != nil { HStack {
HStack { Spacer()
Spacer() FooterButtonView("détails du club") {
Button("modifier", role: .destructive) { showClubDetail = selectedClub
event.club = nil
try? dataStore.events.addOrUpdate(instance: event)
}
} }
} }
} }
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
}
if let selectedClub { if let selectedClub {
Section { ClubCourtSetupView(club: selectedClub, displayContext: selectedClub.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing, selectedCourt: $selectedCourt)
ForEach((0..<tournament.courtCount), id: \.self) { courtIndex in .onChange(of: selectedClub.courtCount) {
_courtView(atIndex: courtIndex, tournamentClub: selectedClub) tournament.courtCount = max(tournament.courtCount, selectedClub.courtCount)
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
}
} }
} }
.onChange(of: tournament.courtCount) { .navigationDestination(item: $showClubDetail) { club in
do { ClubDetailView(club: club, displayContext: club.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
.navigationDestination(item: $selectedCourt) { court in .navigationDestination(item: $selectedCourt) { court in
CourtView(court: court) 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 { #Preview {

@ -63,8 +63,12 @@ struct TournamentGeneralSettingsView: View {
.toolbar { .toolbar {
if textFieldIsFocus { if textFieldIsFocus {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {
Button("Valider") { HStack {
textFieldIsFocus = false 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 SwiftUI
import LeStorage
struct TournamentCellView: View { struct TournamentCellView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -123,7 +124,11 @@ struct TournamentCellView: View {
newTournament.dayDuration = federalTournament.dayDuration newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
newTournament.setupFederalSettings() 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 @ViewBuilder
var body: some View { var body: some View {
Section {
NavigationLink(value: Screen.broadcast) {
LabeledContent {
// Text(tournament.isPrivate ? "privée" : "publique")
} label: {
Text("Publication")
}
}
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
let tournaments = event.tournaments let tournaments = event.tournaments
Section {
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
LabeledContent { LabeledContent {
Text(tournaments.count.formatted() + " épreuve" + tournaments.count.pluralSuffix) Text(tournaments.count.formatted() + " épreuve" + tournaments.count.pluralSuffix)
@ -24,9 +31,6 @@ struct TournamentInitView: View {
} }
} }
} }
}
Section {
NavigationLink(value: Screen.settings) { NavigationLink(value: Screen.settings) {
LabeledContent { LabeledContent {
Text(tournament.settingsDescriptionLocalizedLabel()) Text(tournament.settingsDescriptionLocalizedLabel())
@ -38,29 +42,6 @@ struct TournamentInitView: View {
} footer: { } footer: {
Text("La date, la catégorie, le niveau, le nombre de terrain, les formats, etc.") 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 @ViewBuilder
var body: some View { 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: "en cours", matches: tournament.runningMatches(allMatches))
MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false) MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false)
MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false)

@ -16,6 +16,7 @@ struct TournamentView: View {
var presentationContext: PresentationContext = .agenda var presentationContext: PresentationContext = .agenda
let tournamentSelectionTip = TournamentSelectionTip() let tournamentSelectionTip = TournamentSelectionTip()
let tournamentRunningTip = TournamentRunningTip()
var selectedTournamentId: Binding<String> { Binding( var selectedTournamentId: Binding<String> { Binding(
get: { tournament.id }, get: { tournament.id },
@ -38,39 +39,11 @@ struct TournamentView: View {
var body: some View { var body: some View {
VStack(spacing: 0.0) { VStack(spacing: 0.0) {
List { List {
SubscriptionInfoView() TipView(tournamentRunningTip)
.tipStyle(tint: nil)
if tournament.state() != .canceled { if tournament.state() != .finished {
Section { SubscriptionInfoView()
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)
}
}
} }
switch tournament.state() { switch tournament.state() {
@ -84,15 +57,28 @@ struct TournamentView: View {
Text("todo expliquer cet état") Text("todo expliquer cet état")
} }
case .initial: case .initial:
TournamentInscriptionView(tournament: tournament)
TournamentInitView(tournament: tournament) TournamentInitView(tournament: tournament)
case .build: 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() { if tournament.hasEnded() {
NavigationLink(value: Screen.rankings) { Section {
Text("Classement") NavigationLink(value: Screen.rankings) {
Text("Classement")
}
} }
} }
TournamentRunningView(tournament: tournament)
} }
} }
} }
@ -124,6 +110,8 @@ struct TournamentView: View {
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
EventView(event: event) EventView(event: event)
} }
case .print:
PrintSettingsView(tournament: tournament)
} }
} }
.environment(tournament) .environment(tournament)
@ -155,41 +143,47 @@ struct TournamentView: View {
} }
} }
ToolbarItem(placement: .topBarTrailing) { if presentationContext == .agenda || tournament.state() == .running {
Menu { ToolbarItem(placement: .topBarTrailing) {
if presentationContext == .agenda { Menu {
Button { if presentationContext == .agenda {
navigation.openTournamentInOrganizer(tournament) Button {
} label: { navigation.openTournamentInOrganizer(tournament)
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") } label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
} }
}
Divider() Divider()
if tournament.state() == .build { if tournament.state() == .running {
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Gestion de l'événement") 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 { .onAppear {
TournamentRunningTip.isRunning = tournament.state() == .running
Logger.log("Payment = \(String(describing: self.tournament.payment)), canceled = \(self.tournament.isCanceled)") Logger.log("Payment = \(String(describing: self.tournament.payment)), canceled = \(self.tournament.isCanceled)")
} }
} }

Loading…
Cancel
Save